Mybatis Plus 入坑(含最新3.X配置)
简介
Mybatis-Plus(简称MP)是一个 Mybatis 的增强工具,在 Mybatis 的基础上只做增强不做改变,为简化开发、提高效率而生。
使用它可以简化单表的操作, 节省开发时间, 国人写的文档已经非常通俗易懂了, 所以这里只是对其进行一些规范,便于多人协作开发
如果不了解mp, 请先阅读官方文档, 大约耗时半小时以内
mp最新的mybatis版本是3.4.6, mybatis-spring版本是1.3.2 比我们的全局配置稍微高了一些, 看了下相关的更新以及自测,发现是兼容的, 但是不排除有些操作不支持, 支持的操作已经足够简化开发了
现在mp最新的版本已经升级到3.X, 所以这里2.X的文档不在维护了
名词解释
GlobalConfig -> 指的是mp的全局配置文件, 版本2.X和 3.X的变化还是比较大, 可以直接看源码的注释
BaseMapper -> BaseMapper里面维护了许多常用的方法, 例如根据主键查询记录, 根据根据主键修改记录等等, 普通mapper只需要继承这个BaseMapper即可获得通用的方法
Wrapper -> 条件构造抽象类, 用来生成sql的条件
LogicSqlInjector -> 逻辑sql处理器, (逻辑删除)
PaginationInterceptor -> 分页插件, 底层是物理分页
实体类命名
GlobalConfig 中默认表名、字段名、使用下划线命名
eg. TrainCourse 对应的表名为 train_course
成员 courseType 对应表字段名 course_type
这些可以通过注解覆盖
类 : @TableName("对应的表名")
字段: @TableField("对应的字段名")
@Data
public class Course {
/**
* 主键
*/
@TableId(value = "course_id", type = IdType.AUTO)
private Long courseId;
private Integer courseType;
private String title;
/**
* 城市
*/
private String city;
// 因为mp在使用的时候如果直接使用str, 等于就是使用了魔法值, 不便于维护, 所以统一在实体类中维护
public static final String COURSE_ID = "course_id";
public static final String COURSE_TYPE = "course_type";
}
- 如果是表中不存在的字段 , 务必打上@TableField(exist = false)
- 如果是表示是否删除的字段, 如deleted, 则也要打上@TableLogic 名称和全局一样, 如果和数据库字段名不符合全局的映射, 则需要手动指定
- 数据库中只有0,1的值, 使用Boolean类型
XML和接口
xml和接口是完全兼容之前mybatis的写法, 所以不会影响之前的逻辑。如果xml手动定义的名称和mp的BaseMapper中定义的一样, 则手动指定的会覆盖BaseMapper中的。
即顺序为 自己在xml中定义的方法 > BaseMapper中的方法
如果仅仅使用到了BaseMapper中的方法, 则可以不配置XML配置文件
使用示例
在构建好实体类后, 不要在条件查询器中直接使用魔法值, 不容易维护
// 根据主键id查询
Coursecourse = mCourseMapper.selectById(id);
// 使用查询器
Wrapper<Course> query = new EntityWrapper<>();
if (courseType != null) {
// 这里的写法其实有点像jooq, 生成的sql为: where course_type = courseType
query.eq(Course.COURSE_TYPE, courseType);
}
query.eq(Course.NAME, name);
// deleted 使用 true 或者 false , 尽量不要使用0,1 因为0,1是魔法值
query.eq(Course.DELETED, true);
// 查询count的数量
int result = mCourseMapper.selectCount(query);
逻辑删除插件
一旦使用了逻辑删除插件, 以后默认的select都会带上 deleted = 0, 即只会查出不标记为删除的数据, 所以不需要手动在写
Wrapper<Test> query = new EntityWrapper<>();
query.eq(Test.DELETED, false); //这句话是多余的, 因为配置了逻辑删除插件, 默认在select的时候会加上 deleted = 0 这个条件
// 经过测试发现, 这会带来一个问题, 比如就是要查询删除的记录, 使用下面的这句话是无效的, 所以一旦使用了逻辑删除插件, 如果要查询删除了的记录, 请手写xml
query.eq(Test.DELETED, true); // 效果就是 WHERE deleted=0 AND (deleted = 1) 这样明显是查不出任何一条记录的
2.X版本
示例配置
mp只是用自己的 MybatisSqlSessionFactoryBean 替代了 mybatis-spring的 SqlSessionFactoryBean 其余的配置都不需要改动
再 globalConfig 中, 务必加入逻辑删除插件, 因为我们的大多数操作都是逻辑删除。物理删除请手动写xml。
3.X 版本
3.X版本和2.X版本的差别还是比较大的, 主要的差别如下
升级指南可以参考 https://my.oschina.net/u/241218/blog/1838534
其中有一点, 就是下面这个方法让我有点困惑
UpdateWrapper
方法名 | 说明 |
---|---|
set | SQL SET 字段(一个字段使用一次) |
因为发现在使用中并没有使用到这个方法, 连BaseMapper里面的update, 根据下面的解释, 也用不到, 猜测是要自己实现一个注入方法了, 这里没有深究
配置示例
这里采用java config的方式来配置(个人喜好, 加上java config可以做一些环境判断, 上面那个打印sql每次都需要手动打开注释, 麻烦)
Spring Boot的方式非常简单, 参考官网即可, 所以这里给出的是Spring mvc的配置方式, 使用的DataSource是阿里的druid
这里就直接贴配置文件了, 下面的包含了多数据源的配置
import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.core.MybatisConfiguration;
import com.baomidou.mybatisplus.core.config.GlobalConfig;
import com.baomidou.mybatisplus.core.config.GlobalConfig.DbConfig;
import com.baomidou.mybatisplus.extension.injector.LogicSqlInjector;
import com.baomidou.mybatisplus.extension.plugins.PaginationInterceptor;
import com.baomidou.mybatisplus.extension.plugins.PerformanceInterceptor;
import com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean;
import java.util.ArrayList;
import java.util.List;
import org.apache.ibatis.plugin.Interceptor;
import org.mybatis.spring.mapper.MapperScannerConfigurer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* @author yanghuan
*/
@Configuration
public class MybatisPlusConfig {
// 环境标志, 区分dev or prod
@Autowired
private String projectStage;
/**
* 数据源a 相关信息
*/
@Value("${a.url}")
private String aUrl;
@Value("${a.username}")
private String aUsername;
@Value("${a.password}")
private String aPassword;
/**
* 数据源b 相关信息
*/
@Value("${b.url}")
private String bUrl;
@Value("${b.username}")
private String bUsername;
@Value("${b.password}")
private String bPassword;
// 创建数据源a
@Bean(initMethod = "init", destroyMethod = "close")
public DruidDataSource aDataSource(){
DruidDataSource d = new DruidDataSource();
d.setUrl(aUrl);
d.setUsername(aUsername);
d.setPassword(aPassword);
return d;
}
@Bean(initMethod = "init", destroyMethod = "close")
public DruidDataSource bDataSource(){
DruidDataSource d= new DruidDataSource();
d.setUrl(bUrl);
d.setUsername(bUsername);
d.setPassword(bPassword);
return d;
}
// 创建全局配置
@Bean
public GlobalConfig mpGlobalConfig() {
// 全局配置文件
GlobalConfig globalConfig = new GlobalConfig();
DbConfig dbConfig = new DbConfig();
// 默认为自增
dbConfig.setIdType(IdType.AUTO);
// 手动指定db 的类型, 这里是mysql
dbConfig.setDbType(DbType.MYSQL);
globalConfig.setDbConfig(dbConfig);
if (!ProjectStageUtil.isProd(projectStage)) {
// 如果是dev环境,则使用 reload xml的功能,方便调试
globalConfig.setRefresh(true);
}
// 逻辑删除注入器
LogicSqlInjector injector = new LogicSqlInjector();
globalConfig.setSqlInjector(injector);
return globalConfig;
}
@Bean(name = "aSqlSessionFactory")
public MybatisSqlSessionFactoryBean aSqlSessionFactory(
DruidDataSource aDataSource,
GlobalConfig globalConfig) {
return getSessionFactoryBean(aDataSource, globalConfig);
}
/**
* MapperScannerConfigurer 是 BeanFactoryPostProcessor 的一个实现,如果配置类中出现 BeanFactoryPostProcessor ,会破坏默认的
* post-processing, 如果不加static, 会导致整个都提前加载, 这时候, 取不到projectStage的值
*
* @return
*/
@Bean
public static MapperScannerConfigurer aMapperScannerConfigurer() {
MapperScannerConfigurer configurer = new MapperScannerConfigurer();
configurer.setBasePackage("com.a");
// 设置为上面的 factory name
configurer.setSqlSessionFactoryBeanName("bSqlSessionFactory");
return configurer;
}
@Bean(name = "bSqlSessionFactory")
public MybatisSqlSessionFactoryBean bSqlSessionFactory(
DruidDataSource bDataSource,
GlobalConfig mpGlobalConfig) {
return getSessionFactoryBean(bDataSource, mpGlobalConfig);
}
private MybatisSqlSessionFactoryBean getSessionFactoryBean(
aDataSource aDataSource,
GlobalConfig globalConfig) {
MybatisSqlSessionFactoryBean sqlSessionFactoryBean = new MybatisSqlSessionFactoryBean();
sqlSessionFactoryBean.setDataSource(aDataSource);
sqlSessionFactoryBean.setGlobalConfig(globalConfig);
// 源码里面如果有configuration, 不会注入BaseMapper里面的方法, 所以这里要这样写
MybatisConfiguration configuration = new MybatisConfiguration().init(globalConfig);
configuration.setMapUnderscoreToCamelCase(true);
sqlSessionFactoryBean.setConfiguration(configuration);
List<Interceptor> interceptors = new ArrayList<>();
PaginationInterceptor paginationInterceptor = new PaginationInterceptor();
// 设置分页插件
interceptors.add(paginationInterceptor);
if (!ProjectStageUtil.isProd(projectStage)) {
// 如果是dev环境,打印出sql, 设置sql拦截插件, prod环境不要使用, 会影响性能
PerformanceInterceptor performanceInterceptor = new PerformanceInterceptor();
interceptors.add(performanceInterceptor);
}
sqlSessionFactoryBean.setPlugins(interceptors.toArray(new Interceptor[0]));
return sqlSessionFactoryBean;
}
/**
* b 的mapperscan
* @return
*/
@Bean
public static MapperScannerConfigurer bMapperScannerConfigurer() {
MapperScannerConfigurer configurer = new MapperScannerConfigurer();
configurer.setBasePackage("com.b");
// 设置为上面的 factory name
configurer.setSqlSessionFactoryBeanName("bSqlSessionFactory");
return configurer;
}
}
使用方式和之前的类似, 只不过 EntityWrapper 改为了 QueryWrapper
以及有一些新的lambda方法
- 不需要定义一个静态变量了, 如果在成员变量上打了@TableField注解, 这里会取注解中的值, 否则就是globalConfig里面的
@Test
public void testPluralLambda() {
TableInfoHelper.initTableInfo(null, BdSystemUser.class);
QueryWrapper<BdSystemUser> queryWrapper = new QueryWrapper<>();
queryWrapper.lambda().eq(BdSystemUser::getName,"sss");
queryWrapper.lambda().eq(BdSystemUser::getUserId,1);
BdSystemUser user = mBdSystemUserMapper.selectOne(queryWrapper);
}
- 之前2.X版本对复杂的or查询不能兼容, 即使用 Wrapper 的or方法, 默认是不加括号的
不过对于负责的查询, 还是推荐手写sql, 这里展示一下如何使用mp来构建
@Test
public void test() {
Wrapper<BdSystemUser> wrapper = new QueryWrapper<BdSystemUser>().lambda().eq(BdSystemUser::getName, 123)
.or(c -> c.eq(BdSystemUser::getExternalStaff, 1).eq(BdSystemUser::getUserId, 2))
.eq(BdSystemUser::getUserId, 1);
BdSystemUser user = mBdSystemUserMapper.selectOne(wrapper);
// 对应的sql: SELECT * FROM bdsystem_user WHERE deleted = 0 AND NAME = ? OR ( externalstaff = ? AND userid = ? ) AND userid = ?
// 可以看到, or中的语句是在括号里的
}
3.可以把where 后的条件防止在一个map中
@Test
public void testCompare() {
Map<String, Object> map = new HashMap<>();
map.put("userid", 1);
map.put("ldap", "luoshu");
QueryWrapper<BdSystemUser> queryWrapper = new QueryWrapper<BdSystemUser>()
.allEq(true, map, false);
BdSystemUser user = mBdSystemUserMapper.selectOne(queryWrapper);
// SELECT userid,name,ldap,stafftype,leader,departmenttype,groupid,externalstaff,mobile,created,lastmodified,deleted FROM bdsystem_user WHERE deleted=0 AND ldap = ? AND userid = ?
// 可以看到, map中对应的值, 是where后的条件
}
更详细的demo可以参照 demo
3.X 注意事项
- BaseMapper的selectOne() 现在在返回多条语句的时候,会抛异常, 需要手动在条件中处理, 使用wrapper.last()方法.
- 如果mp和tddl这样的中间件进行集成, 在分页插件的创建时候, 需要指定数据库方言, 之前可以不指定的原因是mp通过解析jdbc url的时候可以获取到对应的方言
PaginationInterceptor paginationInterceptor = new PaginationInterceptor();
// 这边因为接了tddl, 必须手动设置方言, 否则分页会抛异常
paginationInterceptor.setDialectType(DbType.MYSQL.getDb());